-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
TopTotalsCell.swift
385 lines (308 loc) · 14.1 KB
/
TopTotalsCell.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
import UIKit
/// This cell type displays the top data rows for a Stat type, with optional subtitles for the items and data.
/// Ex: Insights Tags and Categories, Period Post and Pages.
/// If there are more than 6 data rows, a View more row is added to display the full list.
/// If a row is tapped, StatsTotalRowDelegate is informed to display the associated detail.
/// If the row has child rows, those child rows are added to the stack view below the selected row.
///
class TopTotalsCell: StatsBaseCell, NibLoadable {
// MARK: - Properties
@IBOutlet weak var outerStackView: UIStackView!
@IBOutlet weak var subtitleStackView: UIStackView!
@IBOutlet weak var rowsStackView: UIStackView!
@IBOutlet weak var itemSubtitleLabel: UILabel!
@IBOutlet weak var dataSubtitleLabel: UILabel!
@IBOutlet weak var topSeparatorLine: UIView!
@IBOutlet weak var bottomSeparatorLine: UIView!
private var topAccessoryView: UIView? = nil {
didSet {
oldValue?.removeFromSuperview()
if let topAccessoryView = topAccessoryView {
outerStackView.insertArrangedSubview(topAccessoryView, at: 0)
topAccessoryView.layoutMargins = subtitleStackView.layoutMargins
outerStackView.setCustomSpacing(Metrics.topAccessoryViewSpacing, after: topAccessoryView)
}
}
}
private var forDetails = false
private var limitRowsDisplayed = true
private let maxChildRowsToDisplay = 10
fileprivate var dataRows = [StatsTotalRowData]()
private var subtitlesProvided = true
private weak var siteStatsInsightsDelegate: SiteStatsInsightsDelegate?
private weak var siteStatsPeriodDelegate: SiteStatsPeriodDelegate?
private weak var siteStatsReferrerDelegate: SiteStatsReferrerDelegate?
private weak var siteStatsDetailsDelegate: SiteStatsDetailsDelegate?
private weak var postStatsDelegate: PostStatsDelegate?
private typealias Style = WPStyleGuide.Stats
// MARK: - Configure
func configure(itemSubtitle: String? = nil,
dataSubtitle: String? = nil,
dataRows: [StatsTotalRowData],
statSection: StatSection? = nil,
siteStatsInsightsDelegate: SiteStatsInsightsDelegate? = nil,
siteStatsPeriodDelegate: SiteStatsPeriodDelegate? = nil,
siteStatsReferrerDelegate: SiteStatsReferrerDelegate? = nil,
siteStatsDetailsDelegate: SiteStatsDetailsDelegate? = nil,
postStatsDelegate: PostStatsDelegate? = nil,
topAccessoryView: UIView? = nil,
limitRowsDisplayed: Bool = true,
forDetails: Bool = false) {
itemSubtitleLabel.text = itemSubtitle
dataSubtitleLabel.text = dataSubtitle
subtitlesProvided = (itemSubtitle != nil && dataSubtitle != nil)
self.dataRows = dataRows
self.statSection = statSection
self.siteStatsInsightsDelegate = siteStatsInsightsDelegate
self.siteStatsPeriodDelegate = siteStatsPeriodDelegate
self.siteStatsReferrerDelegate = siteStatsReferrerDelegate
self.siteStatsDetailsDelegate = siteStatsDetailsDelegate
self.postStatsDelegate = postStatsDelegate
self.topAccessoryView = topAccessoryView
self.limitRowsDisplayed = limitRowsDisplayed
self.forDetails = forDetails
if !forDetails {
addRows(dataRows,
toStackView: rowsStackView,
forType: siteStatsPeriodDelegate != nil ? .period : .insights,
limitRowsDisplayed: limitRowsDisplayed,
rowDelegate: self,
referrerDelegate: self,
viewMoreDelegate: self)
initChildRows()
}
setSubtitleVisibility()
applyStyles()
prepareForVoiceOver()
}
override func prepareForReuse() {
super.prepareForReuse()
rowsStackView.arrangedSubviews.forEach { subview in
// Remove granchild rows
if let row = subview as? StatsTotalRow {
removeChildRowsForRow(row)
}
// Remove child rows
if let childView = subview as? StatsChildRowsView {
removeRowsFromStackView(childView.rowsStackView)
}
}
removeRowsFromStackView(rowsStackView)
}
private enum Metrics {
static let topAccessoryViewSpacing: CGFloat = 32.0
}
}
// MARK: - Private Extension
private extension TopTotalsCell {
func applyStyles() {
Style.configureCell(self)
Style.configureLabelAsSubtitle(itemSubtitleLabel)
Style.configureLabelAsSubtitle(dataSubtitleLabel)
Style.configureViewAsSeparator(topSeparatorLine)
Style.configureViewAsSeparator(bottomSeparatorLine)
}
/// For Overview tables: Hide the subtitles if there is no data or subtitles.
/// For Details table:
/// - Hide the subtitles if none provided.
/// - Hide the stack view.
///
func setSubtitleVisibility() {
subtitleStackView.layoutIfNeeded()
if forDetails {
bottomSeparatorLine.isHidden = true
updateSubtitleConstraints(showSubtitles: subtitlesProvided)
return
}
updateSubtitleConstraints(showSubtitles: !dataRows.isEmpty && subtitlesProvided)
}
private func updateSubtitleConstraints(showSubtitles: Bool) {
if showSubtitles {
subtitleStackView.isHidden = false
} else {
subtitleStackView.isHidden = true
}
}
// MARK: - Child Row Handling
func initChildRows() {
rowsStackView.arrangedSubviews.forEach { subview in
guard let row = subview as? StatsTotalRow else {
return
}
// On the Stats Detail view, do not expand rows initially.
guard siteStatsDetailsDelegate == nil else {
row.expanded = false
return
}
toggleChildRows(for: row, didSelectRow: false)
row.childRowsView?.rowsStackView.arrangedSubviews.forEach { child in
guard let childRow = child as? StatsTotalRow else {
return
}
toggleChildRows(for: childRow, didSelectRow: false)
}
}
}
func addChildRowsForRow(_ row: StatsTotalRow) {
guard let rowIndex = indexForRow(row),
let childRows = row.rowData?.childRows else {
return
}
// Make sure we don't duplicate child rows.
removeChildRowsForRow(row)
// Add child rows to their own stack view,
// store that on the row (for possible removal later),
// and add the child view to the row stack view.
let numberOfRowsToAdd: Int = {
// If this is on Post Stats, don't limit the number of child rows
// as it needs to show a year's worth of data.
if postStatsDelegate != nil {
return childRows.count
}
return childRows.count > maxChildRowsToDisplay ? maxChildRowsToDisplay : childRows.count
}()
let containingStackView = stackViewContainingRow(row)
let childRowsView = StatsChildRowsView.loadFromNib()
for childRowsIndex in 0..<numberOfRowsToAdd {
let childRowData = childRows[childRowsIndex]
let childRow = StatsTotalRow.loadFromNib()
childRow.configure(rowData: childRowData, delegate: self, parentRow: row)
childRow.showSeparator = false
// If this child is just a child, then change the label color.
// If this child is also a parent, then leave the color as default.
if !childRow.hasChildRows {
Style.configureLabelAsChildRowTitle(childRow.itemLabel)
}
childRowsView.rowsStackView.addArrangedSubview(childRow)
}
row.childRowsView = childRowsView
containingStackView?.insertArrangedSubview(childRowsView, at: rowIndex + 1)
}
func removeChildRowsForRow(_ row: StatsTotalRow) {
guard let childRowsView = row.childRowsView,
let childRowsStackView = childRowsView.rowsStackView else {
return
}
// If the row's children have children, remove those too.
childRowsStackView.arrangedSubviews.forEach { subView in
if let subView = subView as? StatsChildRowsView {
removeRowsFromStackView(subView.rowsStackView)
}
}
removeRowsFromStackView(childRowsStackView)
stackViewContainingRow(row)?.removeArrangedSubview(childRowsView)
}
func toggleSeparatorsAroundRow(_ row: StatsTotalRow) {
toggleSeparatorsBeforeRow(row)
toggleSeparatorsAfterRow(row)
}
func toggleSeparatorsBeforeRow(_ row: StatsTotalRow) {
guard let containingStackView = stackViewContainingRow(row),
let rowIndex = indexForRow(row),
(rowIndex - 1) >= 0 else {
return
}
let previousRow = containingStackView.arrangedSubviews[rowIndex - 1]
// Toggle the indented separator line only on top level rows. Children don't show them.
if previousRow is StatsTotalRow && containingStackView == rowsStackView {
(previousRow as! StatsTotalRow).showSeparator = !row.expanded
}
// Toggle the bottom line on the previous stack view
if previousRow is StatsChildRowsView {
(previousRow as! StatsChildRowsView).showBottomSeperatorLine = !row.expanded
}
}
func toggleSeparatorsAfterRow(_ row: StatsTotalRow) {
guard let containingStackView = stackViewContainingRow(row),
let rowIndex = indexForRow(row),
(rowIndex + 1) < containingStackView.arrangedSubviews.count else {
return
}
let nextRow = containingStackView.arrangedSubviews[rowIndex + 1]
// Toggle the indented separator line only on top level rows. Children don't show them.
if nextRow is StatsTotalRow && containingStackView == rowsStackView {
row.showSeparator = !(nextRow as! StatsTotalRow).expanded
}
// If the next row is a stack view, it is the children of this row.
// Proceed to the next parent row, and toggle this row's bottom line
// according to the next parent's expanded state.
if nextRow is StatsChildRowsView {
guard (rowIndex + 2) < containingStackView.arrangedSubviews.count,
let nextParentRow = containingStackView.arrangedSubviews[rowIndex + 2] as? StatsTotalRow else {
return
}
row.childRowsView?.showBottomSeperatorLine = !nextParentRow.expanded
}
}
func indexForRow(_ row: StatsTotalRow) -> Int? {
guard let stackView = stackViewContainingRow(row),
let rowView = stackView.arrangedSubviews.first(where: ({ $0 == row })),
let rowIndex = stackView.arrangedSubviews.firstIndex(of: rowView) else {
return nil
}
return rowIndex
}
func stackViewContainingRow(_ row: StatsTotalRow) -> UIStackView? {
return row.parentRow?.childRowsView?.rowsStackView ?? rowsStackView
}
}
// MARK: - StatsTotalRowDelegate
extension TopTotalsCell: StatsTotalRowDelegate {
func displayWebViewWithURL(_ url: URL) {
siteStatsInsightsDelegate?.displayWebViewWithURL?(url)
siteStatsPeriodDelegate?.displayWebViewWithURL?(url)
siteStatsDetailsDelegate?.displayWebViewWithURL?(url)
}
func displayMediaWithID(_ mediaID: NSNumber) {
siteStatsPeriodDelegate?.displayMediaWithID?(mediaID)
siteStatsDetailsDelegate?.displayMediaWithID?(mediaID)
}
func toggleChildRows(for row: StatsTotalRow, didSelectRow: Bool) {
row.expanded ? addChildRowsForRow(row) : removeChildRowsForRow(row)
toggleSeparatorsAroundRow(row)
siteStatsInsightsDelegate?.expandedRowUpdated?(row, didSelectRow: didSelectRow)
siteStatsPeriodDelegate?.expandedRowUpdated?(row, didSelectRow: didSelectRow)
postStatsDelegate?.expandedRowUpdated?(row, didSelectRow: didSelectRow)
}
func showPostStats(postID: Int, postTitle: String?, postURL: URL?) {
siteStatsPeriodDelegate?.showPostStats?(postID: postID, postTitle: postTitle, postURL: postURL)
siteStatsDetailsDelegate?.showPostStats?(postID: postID, postTitle: postTitle, postURL: postURL)
}
func showAddInsight() {
siteStatsInsightsDelegate?.showAddInsight?()
}
}
// MARK: - StatsTotalRowReferrerDelegate
extension TopTotalsCell: StatsTotalRowReferrerDelegate {
func showReferrerDetails(_ data: StatsTotalRowData) {
siteStatsReferrerDelegate?.showReferrerDetails(data)
}
}
// MARK: - ViewMoreRowDelegate
extension TopTotalsCell: ViewMoreRowDelegate {
func viewMoreSelectedForStatSection(_ statSection: StatSection) {
siteStatsInsightsDelegate?.viewMoreSelectedForStatSection?(statSection)
siteStatsPeriodDelegate?.viewMoreSelectedForStatSection?(statSection)
postStatsDelegate?.viewMoreSelectedForStatSection?(statSection)
}
}
// MARK: - Accessibility
extension TopTotalsCell: Accessible {
func prepareForVoiceOver() {
accessibilityTraits = .summaryElement
guard dataRows.count > 0 else {
return
}
let itemTitle = itemSubtitleLabel.text
let dataTitle = dataSubtitleLabel.text
if let itemTitle = itemTitle, let dataTitle = dataTitle {
let descriptionFormat = NSLocalizedString("Table showing %@ and %@", comment: "Accessibility of stats table. Placeholders will be populated with names of data shown in table.")
accessibilityLabel = String(format: descriptionFormat, itemTitle, dataTitle)
} else {
if let title = (itemTitle ?? dataTitle) {
let descriptionFormat = NSLocalizedString("Table showing %@", comment: "Accessibility of stats table. Placeholder will be populated with name of data shown in table.")
accessibilityLabel = String(format: descriptionFormat, title)
}
}
}
}